package com.asamioffice.goldenport.appengine;

import static com.google.appengine.api.datastore.FetchOptions.Builder.withLimit;
import static com.google.appengine.api.datastore.FetchOptions.Builder.withOffset;
import static com.google.appengine.api.labs.taskqueue.TaskOptions.Builder.url;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.QueryResultList;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;

/**
 * @since   Oct.  2, 2010
 * @version Oct.  2, 2010
 * @author  ASAMI, Tomoharu
 */
public class EntityTools extends AppengineServiceBase {
    private static final int READ_CHUNK_SIZE = 1000;

    private final String kind_name;
    private final DatastoreService datastore_service;
//    private int chunk_size = 10;
//    private long retry_countdown_millis = 3000;
//    private int retry_count = 10;

    protected int chunk_Size() {
        return 10;
    }

    protected boolean use_Datastore_Tx() {
        return false;
    }

    protected boolean use_Task_Queue_Tx() {
//        return true;
        return false;
    }

    protected Entity get_Prototype(PreparedQuery query) {
        List<Entity> entities = query.asList(withLimit(1));
        Entity entity = entities.get(0);
        return entity;
    }

    protected String get_Id(int index) {
        return "id" + index;
    }

    protected String get_Task_Queue_Url() {
        return "/_et";
    }

    private String get_Count_Entities_Command() {
        return "countEntities_" + kind_name;
    }

    protected String[] get_Unique_Column_Value(int offset) {
        return null;
    }

    protected void adjust_Entity(Entity entity, int offset) {
    }

    //
    public EntityTools(String kindName) {
        kind_name = kindName;
        datastore_service = DatastoreServiceFactory.getDatastoreService();
    }

    //
    public void cloneEntity(int index) {
        Date startDateTime = new Date();
        logger.fine("ENTER[" + startDateTime + "] index=" + index);
        clone_entity(index);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.fine("LEAVE[" + ellapse + "]");
    }

    private void clone_entity(int index) {
        Date startDateTime = new Date();
        logger.finer("ENTER[" + startDateTime + "] index=" + index);
        if (is_exists(index)) {
            Date endDateTime = new Date();
            long ellapse = endDateTime.getTime() - startDateTime.getTime();
            logger.warning("IGNORE[" + ellapse + "] index=" + index);
        }
        PreparedQuery query = datastore_service.prepare(get_query());
        Entity prototype = get_Prototype(query);
        Key key = KeyFactory.createKey(kind_name, get_Id(index));
        Entity entity = new Entity(key);
        entity.setPropertiesFrom(prototype);
        adjust_unique_column(entity, index);
        adjust_Entity(entity, index);
        put_entity(entity);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.finer("LEAVE[" + ellapse + "]");
    }

    //
    public void cloneEntities(int count) {
        Date startDateTime = new Date();
        logger.fine("ENTER[" + startDateTime + "] count=" + count);
        clone_entities(0, count);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.fine("LEAVE[" + ellapse + "]");
    }

    public void cloneEntities(int start, int end) {
        Date startDateTime = new Date();
        logger.fine("ENTER[" + startDateTime + "] start=" + start + ", end = " + end);
        clone_entities(start, end);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.fine("LEAVE[" + ellapse + "]");
    }

    private void clone_entities(int start, int end) {
        PreparedQuery query = datastore_service.prepare(get_query());
        Entity entity = get_Prototype(query);
        clone_entities(start, end, entity);
    }

    private void clone_entities(int start, int end, Entity prototype) {
        int tail = start + chunk_Size();
        try {
/*
            if (use_Datastore_Tx()) {
                boolean cont = clone_entity_chunk_tx(start, chunk_Size(), prototype);
                if (!cont) {
                    return;
                }
            } else {
                clone_entity_chunk(start, chunk_Size(), prototype);
            }
*/
            clone_entity_chunk_managed_id(start, chunk_Size(), prototype);
        } catch (RuntimeException e) {
            if (use_Task_Reschedule()) {
                logger.warning("RETRY["
                    + e.getClass().getName()
                    + ":"
                    + e.getMessage()
                    + "] start = "
                    + start
                    + ", tail = "
                    + tail
                    + ", end = "
                    + end);
                if (use_Task_Queue_Tx()) {
                    clone_entity_task_queue_retry_tx(start, end);
                } else {
                    clone_entity_task_queue_retry(start, end);
                }
            } else {
                logger.severe("ERROR["
                    + e.getClass().getName()
                    + ":"
                    + e.getMessage()
                    + "] start = "
                    + start
                    + ", tail = "
                    + tail
                    + ", end = "
                    + end);
                throw e;
            }
            return;
        }
        if (tail < end) {
            if (!is_exists(tail)) {
                clone_entity_task_queue(tail, end);
            }
/*            
            if (use_Task_Queue_Tx()) {
                clone_entity_task_queue_tx(tail, end);
            } else {
                clone_entity_task_queue(tail, end);
            }
*/
        }
    }

    private void clone_entity_task_queue(int start, int end) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                clone_entity_task_queue_core(start, end);
                return;
            } catch (RuntimeException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry["
                        + e.getClass().getName()
                        + ":"
                        + e.getMessage()
                        + "] count="
                        + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void clone_entity_task_queue_tx(int start, int end) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                clone_entity_task_queue_core_tx(start, end);
                return;
            } catch (RuntimeException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry["
                        + e.getClass().getName()
                        + ":"
                        + e.getMessage()
                        + "] count="
                        + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void clone_entity_task_queue_core(int start, int end) {
        Queue queue = QueueFactory.getDefaultQueue();
        logger.finer("start=" + start + ", end=" + end);
        queue.add(url(get_Task_Queue_Url()).param("command", "cloneUsers").param(
            "start",
            String.valueOf(start)).param("end", String.valueOf(end)));
    }

    private void clone_entity_task_queue_core_tx(int start, int end) {
        Transaction tx = datastore_service.beginTransaction();
        try {
            Queue queue = QueueFactory.getDefaultQueue();
            logger.finer("start=" + start + ", end=" + end);
            queue.add(tx, url(get_Task_Queue_Url()).param("command", "cloneUsers").param(
                "start",
                String.valueOf(start)).param("end", String.valueOf(end)));
            tx.commit();
        } catch (RuntimeException e) {
            try {
                if (tx.isActive()) {
                    tx.rollback();
                }
            } catch (RuntimeException e2) {
                logger.warning("rollback exception = "
                    + e2.getClass().getName()
                    + ":"
                    + e2.getMessage());
            }
            throw e;
        }
    }

    private void clone_entity_task_queue_retry(int start, int end) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                clone_entity_task_queue_retry_core(start, end);
                return;
            } catch (RuntimeException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry["
                        + e.getClass().getName()
                        + ":"
                        + e.getMessage()
                        + "] count="
                        + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void clone_entity_task_queue_retry_tx(int start, int end) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                clone_entity_task_queue_retry_core_tx(start, end);
                return;
            } catch (RuntimeException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry["
                        + e.getClass().getName()
                        + ":"
                        + e.getMessage()
                        + "] count="
                        + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void clone_entity_task_queue_retry_core(int start, int end) {
        Queue queue = QueueFactory.getDefaultQueue();
        logger.finer("start=" + start + ", end=" + end);
        queue.add(url(get_Task_Queue_Url()).
            countdownMillis(retry_Countdown_Millis()).
            param("command", "cloneUsre").
            param("start", String.valueOf(start)).
            param("end", String.valueOf(end)));
    }

    private void clone_entity_task_queue_retry_core_tx(int start, int end) {
        Transaction tx = datastore_service.beginTransaction();
        try {
            Queue queue = QueueFactory.getDefaultQueue();
            logger.finer("start=" + start + ", end=" + end);
            queue.add(tx, url(get_Task_Queue_Url())
                .countdownMillis(retry_Countdown_Millis())
                .param("command", "cloneUsre")
                .param("start", String.valueOf(start))
                .param("end", String.valueOf(end)));
        } catch (RuntimeException e) {
            try {
                if (tx.isActive()) {
                    tx.rollback();
                }
            } catch (RuntimeException e2) {
                logger.warning("rollback exception = "
                    + e2.getClass().getName()
                    + ":"
                    + e2.getMessage());
            }
            throw e;
        }
    }

    private void clone_entity_chunk(int start, int chunkSize, Entity prototype) {
        Date startDateTime = new Date();
        logger.finer("ENTER[" + startDateTime + "] start=" + start + ", chunk=" + chunkSize);
        KeyRange ids = datastore_service.allocateIds(kind_name, chunkSize);
        ArrayList<Entity> entities = new ArrayList<Entity>();
        int offset = start;
        for (Key id : ids) {
            Entity entity = new Entity(id);
            entity.setPropertiesFrom(prototype);
            adjust_unique_column(entity, offset);
            adjust_Entity(entity, offset);
            entities.add(entity);
            offset += 1;
        }
        put_entities(entities);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.finer("LEAVE[" + ellapse + "]");
    }

    private void clone_entity_chunk_managed_id(int start, int chunkSize, Entity prototype) {
        Date startDateTime = new Date();
        logger.finer("ENTER[" + startDateTime + "] start=" + start + ", chunk=" + chunkSize);
        if (is_exists(start)) {
            Date endDateTime = new Date();
            long ellapse = endDateTime.getTime() - startDateTime.getTime();
            logger.warning("IGNORE[" + ellapse + "] start=" + start + ", chunk=" + chunkSize);
        }
        Key[] keys = new Key[chunkSize];
        for (int i = 0, index = start;i < chunkSize;i++, index++) {
            keys[i] = KeyFactory.createKey(kind_name, get_Id(index));
        }
        ArrayList<Entity> entities = new ArrayList<Entity>();
        for (int i = 0, index = start;i < chunkSize;i++, index++) {
            Entity entity = new Entity(keys[i]);
            entity.setPropertiesFrom(prototype);
            adjust_unique_column(entity, index);
            adjust_Entity(entity, index);
            entities.add(entity);
        }
        put_entities(entities);
        Date endDateTime = new Date();
        long ellapse = endDateTime.getTime() - startDateTime.getTime();
        logger.finer("LEAVE[" + ellapse + "]");
    }

    private boolean is_exists(int index) {
        boolean r1 = is_exists1(index);
        boolean r2 = is_exists2(index);
        logger.finer("r1=" + r1 + ", r2=" + r2);
        return r1;
    }

    private boolean is_exists1(int index) {
        Key key = KeyFactory.createKey(kind_name, get_Id(index));
        try {
            datastore_service.get(key);
            return true;
        } catch (EntityNotFoundException e) {
            return false;
        }
    }

    private boolean is_exists2(int index) {
        Key key = KeyFactory.createKey(kind_name, get_Id(index));
        Query q = get_query().setKeysOnly().addFilter("__key__", FilterOperator.EQUAL, key);
        Entity entity = datastore_service.prepare(q).asSingleEntity();
        return entity != null;
    }

    private void adjust_unique_column(Entity entity, int offset) {
        String[] columnValue = get_Unique_Column_Value(offset);
        if (columnValue == null) {
            return;
        }
        entity.setProperty(columnValue[0], columnValue[1]);
    }

    private void put_entity(Entity entity) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                datastore_service.put(entity);
                return;
            } catch (DatastoreTimeoutException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry:count=" + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void put_entities(ArrayList<Entity> entities) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                datastore_service.put(entities);
                return;
            } catch (DatastoreTimeoutException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry:count=" + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void put_entities(ArrayList<Entity> entities, Transaction tx) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                datastore_service.put(tx, entities);
                return;
            } catch (DatastoreTimeoutException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry:count=" + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    //
    public long countEntities() {
        long count =
            datastore_service.prepare(get_query().setKeysOnly()).asList(withOffset(0)).size();
        logger.fine(kind_name + " count = " + count);
        return count;
    }

    public long getEntityCount() {
        Query query = new Query("__Stat_Kind__");
        logger.fine(kind_name + " query = " + query);
        query.addFilter("kind_name", Query.FilterOperator.EQUAL, kind_name);
        Entity stat = datastore_service.prepare(query).asSingleEntity();
        logger.fine(kind_name + " stat = " + stat);
        Long count = (Long) stat.getProperty("count");
        logger.fine(kind_name + " count = " + count);
        return count.longValue();
    }

    public String getCountEntitiesCommandName() {
        return get_Count_Entities_Command();
    }

    public long countEntitiesTq() {
        QueryResultList<Entity> list = datastore_service.prepare(get_query().setKeysOnly()).
            asQueryResultList(withLimit(READ_CHUNK_SIZE));
        int count = list.size();
        if (count < READ_CHUNK_SIZE) {
            logger.severe(kind_name + " has " + count + " entities");
            return count;
        }
        logger.fine("count=" + count);
        count_entities_tq(list, count);
        return count;
    }

    public long countEntitiesTq(long totalCount, String encodedString) {
        Cursor cursor = Cursor.fromWebSafeString(encodedString);
        QueryResultList<Entity> list = datastore_service.prepare(get_query().setKeysOnly()).
            asQueryResultList(withLimit(READ_CHUNK_SIZE).cursor(cursor));
        int count = list.size();
        totalCount += count;
        if (count < READ_CHUNK_SIZE) {
            logger.severe(kind_name + " has " + totalCount + " entities");
            return totalCount;
        }
        logger.fine("total count = " + totalCount + ", count=" + count);
        count_entities_tq(list, totalCount);
        return totalCount;
    }

    private void count_entities_tq(QueryResultList<Entity> list, long count) {
        for (int i = 0; i < retry_Count(); i++) {
            try {
                count_entities_tq_core(list, count);
                return;
            } catch (RuntimeException e) {
                int next = i + 1;
                if (next >= retry_Count()) {
                    throw e;
                } else {
                    logger.warning("Retry["
                        + e.getClass().getName()
                        + ":"
                        + e.getMessage()
                        + "] count="
                        + next);
                }
            }
        }
        throw new InternalError("not reached");
    }

    private void count_entities_tq_core(QueryResultList<Entity> list, long count) {
        Queue queue = QueueFactory.getDefaultQueue();
        queue.add(url(get_Task_Queue_Url()).
            param("command", get_Count_Entities_Command()).
            param("count", String.valueOf(count)).
            param("cursor", String.valueOf(list.getCursor().toWebSafeString())));
    }

    public void deleteAllEntries() {
        int sum = 0;
        for (;;) {
            List<Entity> entities =
                datastore_service.prepare(get_query().setKeysOnly()).asList(withOffset(0));
            if (entities.isEmpty()) {
                logger.fine("delete " + sum + " entities");
                return;
            }
            int count = entities.size();
            sum += count;
            ArrayList<Key> keys = new ArrayList<Key>();
            for (Entity entity : entities) {
                keys.add(entity.getKey());
            }
            datastore_service.delete(keys);
            logger.fine("delete " + count + " entities (sum = " + sum + ")");
        }
    }

    //
    protected final Query get_query() {
        return new Query(kind_name);
    }
}
